/*
 *  Copyright © 2018 - 2021 xulianqiang90@163.com. All Rights Reserved.
 */

package io.light.frame.dal.mybatis.generator.core.mapper.func.injector.columns;

import com.google.common.collect.Sets;
import io.light.frame.dal.mybatis.generator.core.cfg.GeneratorConfiguration;
import io.light.frame.dal.mybatis.generator.core.context.GenerationContext;
import io.light.frame.dal.mybatis.generator.core.domain.clazz.Clazz;
import io.light.frame.dal.mybatis.generator.core.domain.mapper.Mapper;
import io.light.frame.dal.mybatis.generator.core.domain.mapper.MapperFunc;
import io.light.frame.dal.mybatis.generator.core.exceptions.MybatisGenException;
import io.light.frame.dal.mybatis.generator.core.mapper.func.injector.FuncInjector;
import io.light.frame.dal.mybatis.generator.core.meta.visit.MetaAccessor;
import io.light.frame.dal.mybatis.generator.core.util.GenToolKit;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.Text;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * Columns injector
 *
 * @author Ivan
 * @version 1.0.0
 * @date 2021-05-22 11:03
 */
@Slf4j
@Component
public class ColumnsInjector extends FuncInjector {

    private static final Set<String> SQL_KEYWORDS = Sets.newHashSet("name");

    private final MetaAccessor metaAccessor;

    private final GeneratorConfiguration configuration;

    public ColumnsInjector(MetaAccessor metaAccessor, GeneratorConfiguration configuration) {
        this.metaAccessor = metaAccessor;
        this.configuration = configuration;
    }

    /**
     * Worked element name
     *
     * @return element name
     * @see Element#getName()
     */
    @Override
    public String workedElement() {
        return "columns";
    }

    /**
     * Accept this function`s element or not
     *
     * @param element    function element
     * @param mapperFunc {@link MapperFunc}
     * @return {@code boolean} accept or not
     */
    @Override
    public boolean canAccept(Element element, MapperFunc mapperFunc) {
        MapperFunc.Type funcType = mapperFunc.getType();
        if (funcType == MapperFunc.Type.delete) {
            throw new MybatisGenException("Unsupported mapper function type: " + funcType);
        }
        List<Element> elements = element.getParent().elements("columns");
        if (elements.size() > 1) {
            throw new MybatisGenException("Only one '<columns>' can be used in function type: " + funcType);
        }
        return true;
    }

    /**
     * Build function columns content
     *
     * @param element    function element
     * @param mapperFunc {@link MapperFunc}
     * @param content    {@link MapperFunc.Content}
     */
    @Override
    public void build(Element element, MapperFunc mapperFunc, MapperFunc.Content content) {
        Mapper mapper = GenerationContext.current().getMapper();
        Columns columns = GenToolKit.elementAttrsAsObject(element, Columns.class);
        List<Columns.Append> appends = columns.getAppends();
        Columns.Scope scope = columns.getScope();
        Clazz entityClazz = mapper.getEntityClazz();
        String thisTableName = mapper.getTable().getName();
        MapperFunc.Type funcType = mapperFunc.getType();
        Map<String, Mapper.Property> propertiesMap = fetchMapperProperties(scope, funcType, mapper, columns);
        if (CollectionUtils.isEmpty(propertiesMap)) {
            return;
        }
        if (funcType == MapperFunc.Type.insert || funcType == MapperFunc.Type.update) {
            if (mapperFunc.getParameterClazz() == null || !entityClazz.getName().equals(mapperFunc.getParameterClazz().getName())) {
                return;
            }
        }
        // for insert
        if (funcType == MapperFunc.Type.insert) {
            buildInsertBlock(content, thisTableName, propertiesMap.values());
            return;
        }
        // for update
        if (funcType == MapperFunc.Type.update) {
            completeSqlPrefix(content, funcType);
            if (content.indexOf(thisTableName) < 0) {
                content.append(" ");
                content.append(thisTableName);
                content.append("\n\t\t");
            }
            content.append("<set>");
            for (Mapper.Property property : propertiesMap.values()) {
                content.append("\n\t\t\t<if test=\"");
                content.append(property.getPropertyName()).append(" != null");
                content.append("\">");
                String colName = property.getColumnName();
                if (SQL_KEYWORDS.contains(colName)) {
                    colName = "`" + colName + "`";
                }
                content.append("\n\t\t\t\t").append(colName);
                content.append(" = ");
                content.append("#{").append(property.getPropertyName()).append("},");
                content.append("\n\t\t\t</if>");
            }
            content.append("\n\t\t</set>");
            return;
        }
        // for select
        if (funcType == MapperFunc.Type.select) {
            completeSqlPrefix(content, funcType);
            if (appends != null && !appends.isEmpty()) {
                appends.forEach(appender -> {
                    try {
                        metaAccessor.touch(metaOperation -> {
                            Mapper tm = new Mapper(metaOperation.table(mapper.getTable().getDatabase(),
                                    appender.getTable()));
                            Map<String, Mapper.Property> propMap = fetchMapperProperties(scope, funcType, tm, appender);
                            if (CollectionUtils.isEmpty(propMap)) {
                                return;
                            }
                            for (Map.Entry<String, Mapper.Property> propEntry : propMap.entrySet()) {
                                String colLabel = propEntry.getKey();
                                Mapper.Property curProp = propEntry.getValue();
                                Mapper.Property existProp = propertiesMap.get(colLabel);
                                if (existProp != null) {
                                    throw new MybatisGenException(String.format(
                                            "Duplicate column label '%s' in function '%s', " +
                                                    "Exist:{tableName: %s, columnName: %s}" +
                                                    ", Conflict:{tableName: %s, columnName: %s}",
                                            colLabel, mapperFunc.getId(),
                                            existProp.getTable().getName(), existProp.getColumnName(),
                                            curProp.getTable().getName(), curProp.getColumnName()
                                    ));
                                }
                                propertiesMap.put(colLabel, curProp);
                            }
                        });
                    } catch (Exception e) {
                        GenToolKit.handleException(log, String.format("Touch metadata failed, database:'%s', table:'%s'",
                                mapper.getTable().getDatabase(), appender.getTable()), e);
                    }
                });
            }
            int i = 0;
            int max = propertiesMap.size() - 1;
            for (Mapper.Property property : propertiesMap.values()) {
                String tableAlias = property.getTableAlias();
                if (tableAlias != null) {
                    content.append(tableAlias).append(".");
                }
                String colName = property.getColumnName();
                if (SQL_KEYWORDS.contains(colName)) {
                    colName = "`" + colName + "`";
                }
                content.append(colName);
                if (tableAlias != null) {
                    String colAlias = property.getColumnName();
                    if (StringUtils.isNotBlank(property.getColumnAlias())) {
                        colAlias = property.getColumnAlias();
                    }
                    content.append(" as ").append(colAlias);
                }
                if (i < max) {
                    content.append(", ");
                }
                i++;
                if (i % 3 == 0 && i <= max) {
                    content.append("\n\t\t\t");
                }
            }
            if (!funcContentHasKeyword(element, "from ", "from\n")) {
                content.append("\n\t\tfrom ").append(thisTableName);
                if (StringUtils.isNotBlank(columns.getTableAlias())) {
                    content.append(" ").append(columns.getTableAlias());
                }
            }
        }
    }

    public void buildInsertBlock(MapperFunc.Content content, String tableName, Collection<Mapper.Property> properties) {
        completeSqlPrefix(content, MapperFunc.Type.insert);
        if (content.indexOf(tableName) < 0) {
            content.append(" into ");
            content.append(tableName);
        }
        content.append(" (\n\t\t");
        content.append("<trim suffixOverrides=\",\">\n\t\t\t");
        int i = 1;
        int mark = 1;
        int max = properties.size();
        boolean endByEnter = false;
        for (Mapper.Property property : properties) {
            String colName = property.getColumnName();
            if (SQL_KEYWORDS.contains(colName)) {
                colName = "`" + colName + "`";
            }
            if (configuration.isSelectiveInsert() && property.hasDefaultValue()) {
                if (!endByEnter) {
                    content.append("\n\t\t\t");
                }
                content.append("<if test=\"").append(property.getPropertyName()).append("!=null\">");
                content.append(colName).append(",");
                content.append("</if>");
                endByEnter = false;
            } else {
                content.append(colName).append(",");
                if (mark % 4 == 0 && i != max) {
                    content.append("\n\t\t\t");
                    endByEnter = true;
                } else {
                    endByEnter = false;
                }
                mark++;
            }
            i++;
        }
        content.append("\n\t\t</trim>");
        content.append("\n\t\t) values (\n\t\t");
        content.append("<trim suffixOverrides=\",\">\n\t\t\t");
        i = 1;
        mark = 1;
        endByEnter = false;
        for (Mapper.Property property : properties) {
            if (configuration.isSelectiveInsert() && property.hasDefaultValue()) {
                if (!endByEnter) {
                    content.append("\n\t\t\t");
                }
                content.append("<if test=\"").append(property.getPropertyName()).append("!=null\">");
                content.append("#{").append(property.getPropertyName()).append("},");
                content.append("</if>");
                endByEnter = false;
            } else {
                content.append("#{").append(property.getPropertyName()).append("},");
                if (mark % 4 == 0 && i != max) {
                    content.append("\n\t\t\t");
                    endByEnter = true;
                } else {
                    endByEnter = false;
                }
                mark++;
            }
            i++;
        }
        content.append("\n\t\t</trim>");
        content.append("\n\t\t)");
    }

    private Map<String, Mapper.Property> fetchMapperProperties(Columns.Scope scope, MapperFunc.Type funcType,
                                                               Mapper mapper, ColumnsNature nature) {
        List<Mapper.Property> mapperProperties;
        if (scope == null || scope == Columns.Scope.auto) {
            switch (funcType) {
                case insert:
                    mapperProperties = mapper.getInsertProperties();
                    break;
                case update:
                    mapperProperties = mapper.getUpdateProperties();
                    break;
                case select:
                    mapperProperties = mapper.getProperties();
                    break;
                default:
                    mapperProperties = null;
                    break;
            }
        } else {
            mapperProperties = mapper.getProperties();
        }
        if (mapperProperties == null || mapperProperties.isEmpty()) {
            return null;
        }
        List<Mapper.Property> properties = new ArrayList<>(mapperProperties);
        List<String> includeCols = StringUtils.isBlank(nature.getIncludeColumns()) ? null
                : Arrays.stream(nature.getIncludeColumns().split(",")).filter(StringUtils::isNotBlank)
                .map(String::trim).distinct().collect(Collectors.toList());
        List<String> excludeCols = StringUtils.isBlank(nature.getExcludeColumns()) ? null
                : Arrays.stream(nature.getExcludeColumns().split(",")).filter(StringUtils::isNotBlank)
                .map(String::trim).distinct().collect(Collectors.toList());
        if (includeCols != null || excludeCols != null) {
            properties.removeIf(property -> {
                if (!CollectionUtils.isEmpty(includeCols)) {
                    return !includeCols.contains(property.getColumnName());
                }
                if (!CollectionUtils.isEmpty(excludeCols)) {
                    return excludeCols.contains(property.getColumnName());
                }
                return false;
            });
        }
        Map<String, ColumnAlias> columnAliasMap = nature.getColumnAliasList() == null
                || nature.getColumnAliasList().isEmpty() ? null : nature.getColumnAliasList().stream()
                .filter(ColumnAlias::isValid)
                .collect(Collectors.toMap(ColumnAlias::getColumn, Function.identity(),
                        (o, n) -> o, LinkedHashMap::new));
        Map<String, Mapper.Property> propertyMap = new LinkedHashMap<>();
        properties.forEach(property -> {
            String tableAlias = StringUtils.isBlank(nature.getTableAlias()) ? null : nature.getTableAlias().trim();
            property.setTableAlias(tableAlias);
            if (!CollectionUtils.isEmpty(columnAliasMap)) {
                ColumnAlias columnAlias = columnAliasMap.get(property.getColumnName());
                if (columnAlias != null) {
                    property.setColumnAlias(columnAlias.getAs());
                }
            }
            if (StringUtils.isNotBlank(property.getColumnAlias())) {
                propertyMap.put(property.getColumnAlias(), property);
            } else {
                propertyMap.put(property.getColumnName(), property);
            }
        });
        return propertyMap;
    }

    private void completeSqlPrefix(MapperFunc.Content content, MapperFunc.Type funcType) {
        if (content.indexOf(funcType.name()) < 0) {
            content.append(" ");
            content.append(funcType.name());
            if (funcType == MapperFunc.Type.select) {
                content.append("\n\t\t\t");
            }
        }
    }

    private boolean funcContentHasKeyword(Element element, String sqlKeyword, String... sqlKeywords) {
        for (Node node : element.getParent().content()) {
            if ("include".equalsIgnoreCase(node.getName())) {
                return true;
            }
            if (!(node instanceof Text)) {
                continue;
            }
            String text = node.getText();
            if (StringUtils.isBlank(text)) {
                continue;
            }
            text = text.toLowerCase();
            if (text.contains(sqlKeyword.toLowerCase())) {
                return true;
            }
            if (sqlKeywords != null && sqlKeywords.length > 0) {
                for (String kw : sqlKeywords) {
                    if (StringUtils.isBlank(kw) && text.contains(kw.toLowerCase())) {
                        return true;
                    }
                }
            }
        }
        return false;
    }
}
